﻿// ===============================================================================
// XmlConfiguration.cs
// EOS Web Framework
// ===============================================================================
// Copyright (c) EOS Development Group. All rights reserved.
// ===============================================================================

namespace EOS.Framework.Configuration
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Xml;
    using System.Xml.Serialization;
    using EOS.Framework.Tools;
    using System.Globalization;
    using System.Diagnostics.CodeAnalysis;
    using EOS.Framework.Contracts.Configuration;
using EOS.Framework.Tools.Aspects;

    /// <summary>
    /// Stores the configuration in a xml file.
    /// </summary>
    public class XmlConfiguration : IConfiguration
    {
        /// <summary>
        /// Reference to the xml dom, representing the configuration file.
        /// </summary>
        private XmlDocument xmlDocument;
        /// <summary>
        /// Indicates if the config file exists.
        /// </summary>
        private bool exists;
        /// <summary>
        /// Name of the file.
        /// </summary>
        private string currentFileName;
        /// <summary>
        /// Indicates, if the config file is loaded.
        /// </summary>
        private bool loaded;

        #region Helper Methods

        /// <summary>
        /// Writes the value to the specified xml element.
        /// </summary>
        /// <typeparam name="T">Type of the value to write.</typeparam>
        /// <param name="element">The element where the value should be written to.</param>
        /// <param name="value">The value to write.</param>
        private void Write<T>(XmlElement element, T value)
        {
            MemoryStream memoryStream = new MemoryStream();

            XmlSerializer serializer = new XmlSerializer(typeof(T));
            serializer.Serialize(memoryStream, value);

            memoryStream.Position = 0;

            XmlDocument tempDocument = new XmlDocument();
            tempDocument.Load(new XmlTextReader(memoryStream));

            XmlElement temp = tempDocument.ChildNodes[1] as XmlElement;
            temp.Attributes.RemoveAll();

            element.AppendChild(xmlDocument.ImportNode(temp, true));
        }


        /// <summary>
        /// Gets the xml child with the specified name.
        /// </summary>
        /// <param name="element">The element, which child should be returned..</param>
        /// <param name="name">The name of the child element..</param>
        /// <returns>The child of the specified element with the given name, if exists, otherwise false.</returns>
        private static XmlElement GetChildByName(XmlElement element, string name)
        {
            XmlElement child = null;

            foreach (XmlNode node in element.ChildNodes)
            {
                XmlElement temp = node as XmlElement;
                if (temp != null && temp.Name == name)
                {
                    child = temp;
                    break;
                }
            }
            return child;
        }

        /// <summary>
        /// Gets the element by path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        private XmlElement GetElementByPath(string path)
        {
            XmlElement current = (XmlElement)xmlDocument.DocumentElement;

            string[] pathElements = path.Split('/');
            for (int i = 0; i < pathElements.Length; i++)
            {
                if ((current = GetChildByName(current, pathElements[i])) == null)
                {
                    throw new ParsingConfigFileException();
                }
            }
            return current;
        }

        /// <summary>
        /// Gets the element by path or create.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="replaceExisting">if set to <c>true</c> [replace existing].</param>
        /// <returns></returns>
        private XmlElement GetElementByPathOrCreate(string path, bool replaceExisting)
        {
            XmlElement current = (XmlElement)xmlDocument.DocumentElement;

            string[] pathElements = path.Split('/');
            for (int i = 0; i < pathElements.Length; i++)
            {
                XmlElement temp = GetChildByName(current, pathElements[i]);
                if (temp == null)
                {
                    temp = xmlDocument.CreateElement(pathElements[i]);
                    current.AppendChild(temp);
                }  
                current = temp;
            }
            if (replaceExisting == true)
            {
                current.RemoveAll();
            }

            return current;
        }

        /// <summary>
        /// Gets the short name for the specified type.
        /// </summary>
        /// <param name="type">The type, which short name should be calculated.</param>
        /// <returns>The name for the specified type.</returns>
        private static string GetShortName(Type type)
        {
            string result = type.Name;
            if (type == typeof(byte))
            {
                result = "byte";
            }
            else if (type == typeof(char))
            {
                result = "char";
            }            
            else if (type == typeof(short))
            {
                result = "short";
            }
            else if (type == typeof(int))
            {
                result = "int";
            }
            else if (type == typeof(long))
            {
                result = "long";
            }
            else if (type == typeof(float))
            {
                result = "float";
            }
            else if (type == typeof(double))
            {
                result = "double";
            }
            else if (type == typeof(string))
            {
                result = "string";
            }

            return result;
        }

        /// <summary>
        /// Reads a value with the specified type from the the given xml element.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="current">The current xml element, where the value should be read from.</param>
        /// <returns>The read value.</returns>
        private static T Read<T>(XmlElement current)
        {
            Type targetType = typeof(T);

            try
            {
                return (T)Convert.ChangeType(current.InnerText, targetType, CultureInfo.CurrentUICulture);
            }
            catch (InvalidCastException)
            {
                MemoryStream memoryStream = new MemoryStream();

                XmlDocument tempDocument = new XmlDocument();
                tempDocument.AppendChild(tempDocument.ImportNode(current, true));
                tempDocument.Save(memoryStream);
                memoryStream.Position = 0;

                memoryStream.Position = 0;

                XmlSerializer serializer = new XmlSerializer(typeof(T));
                try
                {
                    return (T)serializer.Deserialize(memoryStream);
                }
                catch (InvalidOperationException inner)
                {
                    throw new ParsingConfigFileException(
                        "Object cannot be parsed!", inner);
                }
            }
        }

        #endregion

        #region IConfiguration Member

        /// <summary>
        /// True, if already loaded, else false.
        /// </summary>
        /// <value></value>
        public bool Loaded
        {
            get { return loaded; }
        }

        /// <summary>
        /// Loads the configuration with the specified file name.
        /// </summary>
        /// <param name="fileName">Name of the file. Cannot be null or empty.</param>
        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is
        /// null (Nothing in Visual Basic).</exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> is a string
        /// with length 0 or contains only blanks.</exception>
        /// <exception cref="InvalidOperationException">Configuration file is already loaded.</exception>
        /// <exception cref="FileNotFoundException">Configuration file does not exist.</exception>
        [ParamNotNullOrEmpty("fileName")]
        public void Load(string fileName)
        {
            if (loaded)
            {
                throw new InvalidOperationException("Configuration is already loaded.");
            }

            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException();
            }

            this.currentFileName = fileName;
            using (Stream stream = new FileStream(fileName, FileMode.Open))
            {
                xmlDocument = new XmlDocument();
                try
                {
                    xmlDocument.Load(new XmlTextReader(stream));

                    if (xmlDocument.DocumentElement == null || xmlDocument.DocumentElement.Name != "Config")
                    {
                        exists = false;
                        throw new ArgumentException(
                            "Configuration file is not a valid xml. The Root must be \"Config\"");
                    }
                    else
                    {
                        exists = true;
                    }
                }
                catch (XmlException inner)
                {
                    throw new ArgumentException(
                        "Configuration file is not a valid xml. The Root must be \"Config\"");
                }
                finally
                {
                    stream.Close();
                }
            }

            loaded = true;
        }

        /// <summary>
        /// Adds a value to the config file at the specified path.
        /// </summary>
        /// <typeparam name="T">Type of the value to add.</typeparam>
        /// <param name="path">The path, where the value should be stored.</param>
        /// <param name="value">The value to store.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="path"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <para><paramref name="fileName"/> is a string with length 0 or contains only blanks.</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="value"/> is a collection.</para>
        /// </exception>
        [ParamNotNullOrEmpty("path"), ParamNotNull("value")]
        public void AddValue<T>(string path, T value)
        {
            VerfiyConfigExists();

            if (typeof(ICollection).IsAssignableFrom(typeof(T)))
            {
                throw new ArgumentException(
                    "Collections not allowed for value, use SetValueList.");
            }

            XmlElement current = GetElementByPathOrCreate(path, false);
            Write<T>(current, value);
        }

        /// <summary>
        /// Sets the value at the specified path.
        /// </summary>
        /// <typeparam name="T">Type of the value to add.</typeparam>
        /// <param name="path">The path, where the value should be stored.</param>
        /// <param name="value">The value to store.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="path"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="value"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <para><paramref name="fileName"/> is a string with length 0 or contains only blanks.</para>
        ///     <para>- or -</para>
        ///     <para><paramref name="value"/> is a collection.</para>
        /// </exception>
        [ParamNotNullOrEmpty("path"), ParamNotNull("value")]
        public void SetValue<T>(string path, T value)
        {
            VerfiyConfigExists();

            if (typeof(ICollection).IsAssignableFrom(typeof(T)))
            {
                throw new ArgumentException(
                    "Collections not allowed for value, use SetValueList.");
            }

            XmlElement current = GetElementByPathOrCreate(path, true);
            Write<T>(current, value);

            using (Stream stream = new FileStream(currentFileName, FileMode.Open))
            {
                xmlDocument.Save(stream);
                stream.Close();
            }
        }

        /// <summary>
        /// Sets the value list at the specified apth.
        /// </summary>
        /// <typeparam name="T">Type of an list item.</typeparam>
        /// <param name="path">The path, where the list should be stored..</param>
        /// <param name="list">The list to add.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="path"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="list"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> is a string
        /// with length 0 or contains only blanks.</exception>
        [SuppressMessage("Microsoft.Design", "CA1002")]
        [ParamNotNullOrEmpty("path"), ParamNotNull("list")]
        public void SetValueList<T>(string path, List<T> list)
        {
            VerfiyConfigExists();

            XmlElement current = GetElementByPathOrCreate(path, true);
            foreach (T t in list)
            {
                Write<T>(current, t);
            }

            using (Stream stream = new FileStream(currentFileName, FileMode.Open))
            {
                xmlDocument.Save(stream);
                stream.Close();
            }
        }

        /// <summary>
        /// Gets the value at the specified path.
        /// </summary>
        /// <typeparam name="T">Type of the value.</typeparam>
        /// <param name="path">The path.</param>
        /// <returns>The value at the path.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is
        /// null (Nothing in Visual Basic).</exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> is a string
        /// with length 0 or contains only blanks.</exception>
        [SuppressMessage("Microsoft.Design", "CA1004")]
        [ParamNotNullOrEmpty("path")]
        public T GetValue<T>(string path)
        {
            VerfiyConfigExists();

            if (default(T) is ICollection)
            {
                throw new ArgumentException("Collections not allowed, use GetValueList instead");
            }

            try
            {
                XmlElement current = GetElementByPath(path) as XmlElement;
                return Read<T>(GetChildByName(current, GetShortName(typeof(T))));
            }
            catch (Exception innerException)
            {
                throw new ParsingConfigFileException("", innerException);
            }
        }

        /// <summary>
        /// Sets the value list at the specified apth.
        /// </summary>
        /// <typeparam name="T">Type of an list item.</typeparam>
        /// <param name="path">The path, where the list should be stored..</param>
        /// <param name="list">The list to add.</param>
        /// <exception cref="ArgumentNullException">
        /// 	<para><paramref name="path"/> is null (Nothing in Visual Basic).</para>
        /// 	<para>- or -</para>
        /// 	<para><paramref name="list"/> is null (Nothing in Visual Basic).</para>
        /// </exception>
        /// <exception cref="ArgumentException"><paramref name="fileName"/> is a string
        /// with length 0 or contains only blanks.</exception>
        [SuppressMessage("Microsoft.Design", "CA1002")]
        [SuppressMessage("Microsoft.Design", "CA1004")]
        [ParamNotNullOrEmpty("path")]
        public List<T> GetValueList<T>(string path)
        {
            VerfiyConfigExists();   

            List<T> list = new List<T>();
            foreach (XmlNode node in GetElementByPath(path).ChildNodes)
            {
                if (node.Name.Equals(GetShortName(typeof(T))))
                {
                    list.Add(Read<T>(node as XmlElement));
                }
            }
            return list;
        }

        /// <summary>
        /// Verifies that the config file exists.
        /// </summary>
        private void VerfiyConfigExists()
        {
            if (!exists)
            {
                throw new ParsingConfigFileException(
                    "Config file does not exist!");
            }
        }

        #endregion
    }
}
